<!DOCTYPE HTML>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
    <meta charset="utf-8" />
    <title>Gaiman Programming Language Playground</title>
    <meta name="Description" content="Demo of the Gaiman DSL programming language. Storytellig game engine"/>
    <link rel="apple-touch-icon" sizes="180x180" href="/favicon/apple-touch-icon.png"/>
    <link rel="icon" type="image/png" sizes="32x32" href="/favicon/favicon-32x32.png"/>
    <link rel="icon" type="image/png" sizes="16x16" href="/favicon/favicon-16x16.png"/>
    <link rel="manifest" href="/favicon/site.webmanifest"/>
    <link rel="mask-icon" href="/favicon/safari-pinned-tab.svg" color="#5bbad5"/>
    <link rel="shortcut icon" href="/favicon/favicon.ico"/>
    <meta name="msapplication-TileColor" content="#ffc40d"/>
    <meta name="msapplication-config" content="/favicon/browserconfig.xml"/>
    <meta name="theme-color" content="#ffffff"/>
    <!--[if IE]>
    <script src="http://html5shim.googlecode.com/svn/trunk/html5.js"></script>
    <![endif]-->
    <meta name="twitter:card" content="summary_large_image"/>
    <meta name="twitter:site" content="@jcubic"/>
    <meta name="twitter:title" content="Gaiman Programming Language Playground"/>
    <meta name="twitter:description" content="The minimalistic programming language, based on Ruby, which helps create web-based terminal applications."/>
    <meta name="twitter:image" content="https://cdn.jsdelivr.net/gh/jcubic/gaiman@master/assets/banner.svg"/>
    <meta property="og:title" content="Gaiman Programming Language Playground">
    <meta property="og:site_name" content="Gaiman Playground"/>
    <meta property="og:url" content="https://gaiman.js.org/"/>
    <meta property="og:description" content="The minimalistic programming language, based on Ruby, which helps create web-based terminal applications."/>
    <meta property="og:type" content="website"/>
    <meta property="og:image" content="https://cdn.jsdelivr.net/gh/jcubic/gaiman@master/assets/banner.svg"/>
    <link href="https://cdn.jsdelivr.net/npm/jquery.splitter/css/jquery.splitter.css" rel="stylesheet"/>
    <link href="https://cdn.jsdelivr.net/npm/codemirror@5.x.x/lib/codemirror.css" rel="stylesheet"/>
    <link href="https://cdn.jsdelivr.net/combine/npm/codemirror@5.x.x/addon/search/matchesonscrollbar.css,npm/codemirror@5.x.x/addon/dialog/dialog.css" rel="stylesheet"/>
    <link href="https://cdn.jsdelivr.net/npm/codemirror@5.x.x/theme/seti.css" rel="stylesheet"/>
    <style>
     :root {
         --separator: gray;
         --tab-bg: #1d1e22;
         --tab-color: #aaaebc;
         --tab-border-color: #34363e;
         --tab-color-active: #1236f7;
     }
     body {
         margin: 0;
     }
     header {
         background: black;
         font-family: sans-serif;
         color: white;
         padding: 8px 10px;
         border-bottom: 2px solid var(--separator);
         display: flex;
         align-items: center;
         justify-content: space-between;
     }
     header h1 {
         margin: 0;
         text-indent: -999999999px;
         width: 120px;
         height: 35px;
         background: black url(./brand-white.svg);
         background-size: contain;
     }
     main {
         height: calc(100vh - 30px - 53px);
     }
     iframe {
         border: none;
         background: black;
         display: block;
     }
     .CodeMirror {
         height: 100%;
     }
     .editor, .ast-explorer, .gaiman-output, .user-input, iframe {
         width: 100%;
         height: 100%;
     }
     .hidden {
         display: none !important;
     }
     .errors ul, .tabs > ul, .config ul {
         list-style: none;
         margin: 0;
         padding: 0;
     }
     .tabs > ul {
         padding: 5px 10px 0 5px;
         background: black;
         font-family: sans-serif;
         border-bottom: 2px solid var(--separator);
     }
     .tabs > ul li {
         background: var(--tab-bg);
         color: var(--tab-color);
         border-top: 3px solid var(--tab-border-color);
         border-radius: 3px 3px 0 0;
         display: inline-block;
     }
     .tabs > .content > :not(.active) {
         display: none;
     }
     .tabs > .content {
         height: calc(100% - 40px);
     }
     .tabs > ul li.active {
         border-top-color: var(--tab-color-active);
         margin-bottom: -2px;
         padding-bottom: 2px;
     }
     .tabs > ul li a, .tabs > ul li a:hover {
         color: inherit;
         display: block;
         padding: 6px 18px;
         text-decoration: none;
     }
     @media screen and (min-width: 900px) {
         iframe, aside {
             height: 100%;
         }
     }
     .CodeMirror-vscrollbar::-webkit-scrollbar {
         width: 6px;
         height: 6px;
         background: var(--background, #000);
     }
     .CodeMirror-vscrollbar::-webkit-scrollbar-thumb {
         background: var(--color, #aaa);
     }
     .CodeMirror-vscrollbar::-webkit-scrollbar-thumb:hover {
         background: var(--color, #aaa);
     }
     .CodeMirror-vscrollbar {
         scrollbar-color: #aaa #000;
         scrollbar-color: var(--color, #aaa) var(--background, #000);
         scrollbar-width: thin;
     }
     .CodeMirror .syntax-error {
         background: red !important;
         color: black !important;
     }
     .cm-s-seti span.cm-string, .cm-s-seti span.cm-string-2 {
         color: #1c9d34;
     }
     .config {
         position: fixed;
         bottom: 27px;
         right: 0;
         pointer-events: none;
         padding: 10px 20px;
         border-radius: 5px 0 0 0;
         color: gray;
         background: rgba(0, 0, 0, 0.5);
         font-family: sans-serif;
         text-align: right;
         z-index: 100;
     }
     .config ul {
         pointer-events: visible;
     }
     .config li {
         display: block;
         margin: 5px;
     }
     .config button {
         width: 100%;
     }
     .errors {
         color: white;
         position: absolute;
         bottom: 0;
         right: 0;
         left: 0;
         z-index: 200;
     }
     .errors ul:not(:empty) {
         margin: 10px;
     }
     .errors li {
         position: relative;
         background: #CA1919;
         font-family: monospace;
         padding: 5px 20px 5px 10px;
         margin: 5px 0;
         box-shadow: 1px 2px 11px 2px rgba(0,0,0,0.42);
     }
     .errors .message {
         white-space: pre-wrap;
     }
     .errors .close {
         cursor: pointer;
         position: absolute;
         top: 5px;
         right: 5px;
         background: black;
         width: 16px;
         height: 16px;
         line-height: 14px;
         font-size: 16px;
         display: flex;
         justify-content: center;
         align-items: center;
         border-radius: 50%;
     }
     .btn {
         box-shadow:inset 0px -3px 7px 0px #29bbff;
         background:linear-gradient(to bottom, #2dabf9 5%, #0688fa 100%);
         background-color:#2dabf9;
         border-radius:3px;
         border:1px solid #0b0e07;
         display:inline-block;
         cursor:pointer;
         color:#ffffff;
         font-family:Arial, sans-serif;
         font-size:15px;
         padding:9px 23px;
         text-decoration:none;
         text-shadow:0px 1px 0px #263666;
     }
     .btn:hover {
         background:linear-gradient(to bottom, #0688fa 5%, #2dabf9 100%);
         background-color:#0688fa;
     }
     .btn:active {
         position:relative;
         top:1px;
     }
     footer {
         padding: 5px 0;
         font-family: monospace;
         text-align: center;
         background: var(--background, #000);
         color: var(--color, #ccc);
         border-top: 2px solid grey;
     }
     footer p {
         margin: 0;
     }
     footer a[href] {
         color: #3377FF;
         color: var(--link-color, #3377FF);
         cursor: pointer;
     }
     footer a[href]:hover {
         background-color: #3377FF;
         background-color: var(--link-color, #3377FF) !important;
         color: #000;
         color: var(--background, #000) !important;
         text-decoration: none;
     }
    </style>
    <script src="https://cdn.jsdelivr.net/npm/jquery"></script>
    <script src="https://cdn.jsdelivr.net/npm/codemirror@5.x.x/lib/codemirror.js"></script>
    <script src="https://cdn.jsdelivr.net/combine/npm/codemirror@5.x.x/addon/search/search.js,npm/codemirror@5.x.x/addon/search/matchesonscrollbar.js,npm/codemirror@5.x.x/addon/search/searchcursor.js,npm/codemirror@5.x.x/addon/search/jump-to-line.js,npm/codemirror@5.x.x/addon/scroll/annotatescrollbar.js,npm/codemirror@5.x.x/addon/edit/matchbrackets.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/codemirror@5.x.x/addon/dialog/dialog.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/peggy"></script>
    <script src="https://cdn.jsdelivr.net/combine/npm/codemirror@5.x.x/mode/javascript/javascript.js,npm/codemirror@5.x.x/mode/pegjs/pegjs.js,npm/codemirror@5.x.x/mode/css/css.js"></script>
    <script src="https://cdn.jsdelivr.net/combine/npm/codemirror@5.65.0/mode/xml/xml.js,npm/codemirror@5.65.0/mode/htmlmixed/htmlmixed.js"></script>
    <script src="https://cdn.jsdelivr.net/gh/jcubic/gaiman@master/src/codemirror.js"></script>
    <!--
    <script src="../src/codemirror.js"></script>
    -->
    <script src="https://cdn.jsdelivr.net/npm/jquery.splitter"></script>
    <script src="https://cdn.jsdelivr.net/gh/jcubic/static/js/idb-keyval.js"></script>
    <script src="https://cdn.jsdelivr.net/gh/jcubic/gaiman@master/umd.min.js"></script>
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
</head>
<body>
  <header>
    <h1>Gaiman Programming Language and Text Advanture Game Engine</h1>
    <div class="right">
      <label for="theme">Theme:</label>
      <select id="theme"></select>
      <label for="example">Example:</label>
      <select id="example"></select>
    </div>
  </header>
  <main>
  <div class="tabs">
    <ul>
      <li class="active"><a href="#">Preview</a></li>
      <li class="hidden dev"><a href="#">JavaScript</a></li>
      <li class="hidden dev"><a href="#">AST</a></li>
      <li class="hidden dev"><a href="#">Esprima</a></li>
    </ul>
    <div class="content">
      <div class="gaiman-output active">
        <iframe id="frame" src="./demo/"></iframe>
        <div class="errors">
          <ul></ul>
        </div>
      </div>
      <div class="output editor dev"></div>
      <div class="ast editor dev"></div>
      <div class="dev ast-explorer">
        <div class="explorer-js editor"></div>
        <div class="explorer-ast editor"></div>
      </div>
    </div>
  </div>
  <aside class="editor-wrapper">
    <div class="tabs dev hidden">
      <ul>
        <li class="active"><a href="#">PEG (Peggy) grammar</a></li>
        <li><a href="#">CodeMirror</a></li>
      </ul>
      <div class="content">
        <div class="gaiman-grammar active editor">
          <div class="errors">
            <ul></ul>
          </div>
        </div>
        <div class="peg-cm-editor editor"></div>
      </div>
    </div>
    <div class="tabs user-input">
      <ul>
        <li class="active"><a href="#">Gaiman</a></li>
        <li><a href="#">CSS</a></li>
        <li><a href="#">HTML</a></li>
        <li><a href="#" class="dev hidden">JavaScript</a></li>
        <li><a href="#" class="dev hidden">CodeMirror</a></li>
      </ul>
      <div class="content">
        <div class="gaiman-code editor active">
          <div class="errors">
            <ul></ul>
          </div>
        </div>
        <div class="css-editor editor"></div>
        <div class="html-editor editor"></div>
        <div class="js-editor editor"></div>
        <div class="cm-editor editor"></div>
      </div>
    </div>
  </aside>
</main>
<footer>
  <p>Copyright (C) 2021-2024 <a href="https://jakub.jankiewicz.org">Jakub T. Jankiewicz</a> - <a href="https://github.com/jcubic/gaiman">Source Code</a></p>
</footer>
<div class="config">
  <ul>
    <li class="hidden dev">
      <label for="full-output">Full Output</label>
      <input type="checkbox" id="full-output"/>
    </li>
    <li>
      <label for="developer-mode">Dev Mode</label>
      <input type="checkbox" id="developer-mode"/>
    </li>
    <li>
      <button id="reset" class="btn">Reset</button>
    </li>
    <li>
      <button id="download" class="btn">Download</button>
    </li>
  </ul>
</div>
<script type="text/x-template" id="css">:root {
    --color: #ccc;
    --background: black;
    --link-color: #3377FF;
    --size: 1;
    /* use --glow: 1; to add glow effect */
    /* available animations: terminal-bar, terminal-underline */
    --animation: terminal-blink;
    --font: monospace;
    --options: {}; /* JSON config for jQuery Terminal */
}
body {
    margin: 0;
    padding: 0;
}
#term {
    height: 100vh;
}</script>
<script type="text/x-template" id="html"><div id="term"></div></script>
<script>
 var __GAIMAN_PLAYGROUND__ = true;

 $.fn.cm_refresh = function() {
     return this.filter('.CodeMirror').each(function() {
         this.CodeMirror.refresh();
     });
 };

 const urlSearchParams = new URLSearchParams(window.location.search);
 const query = Object.fromEntries(urlSearchParams.entries());

 function parse_cookies(cookies) {
     const result = {};
     cookies.split(/\s*;\s*/).forEach(function(pair) {
         pair = pair.split(/\s*=\s*/);
         var name = decodeURIComponent(pair[0]);
         var value = decodeURIComponent(pair.splice(1).join('='));
         result[name] = value;
     });
     return result;
 }

 // ref: https://www.freecodecamp.org/news/javascript-debounce-example/
 function debounce(func, timeout = 300) {
     let timer;
     return (...args) => {
         clearTimeout(timer);
         timer = setTimeout(() => { func.apply(this, args); }, timeout);
     };
 }

 function css_sanitize(css) {
     var style = document.createElement('style');
     style.innerHTML = css;
     document.head.appendChild(style);
     var sheet = style.sheet;
     var result = Array.from(style.sheet.cssRules).map(rule => {
         return rule.cssText || '';
     }).join('\n');
     document.head.removeChild(style);
     return result;
 }

 async function hash(branch) {
     try {
         var url = `https://api.github.com/repos/jcubic/gaiman/commits?sha=${branch}`;
         var res = await fetch(url);
         var data = await res.json();
         return data[0].sha;
     } catch(e) {
         return branch;
     }
 }

 function tab_activate(li) {
     var $li = $(li);
     var index = $li.index();
     $li.addClass('active').siblings().removeClass('active');
     var $content = $li.closest('ul').next();
     var $tab = $content.children()
                        .eq(index)
                        .addClass('active');
     $tab.siblings()
         .removeClass('active');
     if ($tab.is('.ast-explorer') && !$tab.is('.splitter_panel')) {
         $('.ast-explorer').split({
             orientation: 'horizontal'
         });
     }
     $tab.find('.CodeMirror').cm_refresh();
 }

 var worker;
 if ('serviceWorker' in navigator) {
     var scope = location.pathname.replace(/\/[^\/]+$/, '/');
     worker = navigator.serviceWorker.register('sw.js', {scope})
              .then(function(reg) {
                  reg.addEventListener('updatefound', function() {
                      var installing_worker = reg.installing;
                      console.log('A new service worker is being installed:',
                                  installing_worker);
                  });
                  // registration worked
                  console.log('Registration succeeded. Scope is ' + reg.scope);
              }).catch(function(error) {
                  // registration failed
                  console.log('Registration failed with ' + error);
              });
 }

 var ls = {
     get: function(name) {
         var value = localStorage.getItem(name);
         if (value) {
             return JSON.parse(value);
         }
         return value;
     },
     set: function(name, value) {
         localStorage.setItem(name, value);
     }
 };

 function fetch_text(url) {
     return fetch(url).then(res => res.text());
 }

 const query_mapping = {};

 async function get_file(token, url, file = '', fallback = () => fetch_text(url + file)) {
     async function next() {
         let result = await idbKeyval.get(token);
         if (!result) {
             result = await fallback();
         }
         return result;
     }
     try {
         if (query_mapping[token]) {
             const prop = query_mapping[token];
             const file = query[prop];
             if (file) {
                 return await fetch_text(url + file);
             }
         }
         return next();
     } catch(e) {
         return next();
     }
 }

 function get_template(token, url, node) {
     return get_file(token, url, null, () => node.innerHTML);
 }

 async function get_css(repo) {
     if (query.css) {
         return fetch_text(`${repo}examples/css/${query.css}`);
     }
     return (await get(CSS_FILE)) || css.innerHTML;
 }

 function to_json(object) {
     return JSON.stringify(object, true, 2);
 }

 function make_editor(element, code, options) {
     var $node = $(element);
     const settings = $.extend({
         value: code,
         theme: 'seti',
         lineWrapping: true,
         lineNumbers: true,
         matchBrackets: true,
         extraKeys: { Tab: better_tab, "Alt-F": "findPersistent" },
         readOnly: true,
         indentUnit: 4,
         mode: 'javascript'
     }, options);
     return CodeMirror($node.get(0), settings);
 }

 function clear_marks(editor) {
     const loc = { line: 0, ch: 0 };
     const doc = editor.getDoc();
     doc.markText(loc, loc);
 }

 function mark_error(editor, message, start, end) {
     const doc = editor.getDoc();
     doc.markText(start, end, {
         className: 'syntax-error'
     })
 }

 function error(editor, message) {
     var $editor = $(editor);
     var ul = $editor.find('.errors ul');
     var found = ul.find('> li').filter(function() {
         return $(this).data('message') === message;
     });
     if (!found.length) {
         var $li = $(`<li>
              <span class="message">${message}</span>
              <span class="close">&times;</span>
           </li>`).data('message', message);
         $li.appendTo(ul)
     }
 }


 async function list_directory(user, repo, directory) {
     const url = `https://api.github.com/repos/${user}/${repo}/git/trees/master`;
     directory = directory.split('/').filter(Boolean);
     const dir = await directory.reduce(async (acc, dir) => {
         const { url } = await acc;
         const list = await fetch(url).then(res => res.json());
         return list.tree.find(node => node.path === dir);
     }, { url });
     if (dir) {
         const list = await fetch(dir.url).then(res => res.json());
         return list.tree.map(node => node.path);
     }
 }

 const TIMEOUT = 3600;

 async function get_examples(path = 'examples', filter = /\.gs$/, timeout = TIMEOUT) {
     async function fetch() {
         const examples = (await list_directory('jcubic', 'gaiman', path)).filter(name => {
             return name.match(filter);
         });
         await idbKeyval.set(key, { time: timestamp, examples });
         return examples;
     }
     const key = path;
     const timestamp = +new Date();
     const data = await idbKeyval.get(key);
     if (!data) {
         return fetch();
     } else {
         let { time, examples } = data;
         if (timestamp + timeout > time) {
             return fetch();
         }
         return examples;
     }
 }

 function clear_error(editor) {
     var $editor = $(editor);
     $editor.find('.errors ul').empty();
 }

 function parse_error(code, e) {
     if (!code) {
         clear_error('.gaiman-code');
     } else {
         error('.gaiman-code', format_error(code, e));
     }
 }

 function mark_error_char(location) {
     const { start, end } = location;
     let col = start.column;
     if (end.column === col) {
         col--;
     }
     const range = end.column - col;
     return ' '.repeat(col - 1) + '^'.repeat(range);
 }

 function format_error(code, e) {
     const output = ['Parse Error: ' + e.message, ''];
     if (e.location) {
         try {
             output.push('Error in line ' + e.location.start.line);
             const lines = code.split('\n');
             const line_number = e.location.start.line - 1;
             output.push(lines[line_number]);
             output.push(mark_error_char(e.location));
         } catch (e) {
             // ignore errors in print
         }
     }
     return output.join('\n');
 }

 function better_tab(cm) {
     if (cm.somethingSelected()) {
         cm.indentSelection("add");
     } else {
         var spaces = Array(cm.getOption("indentUnit") + 1).join(" ");
         cm.replaceSelection(
             cm.getOption("indentWithTabs")? "\t" : spaces,
             "end",
             "+input"
         );
     }
 }

 function update_editor(cm, code) {
     const { left, top } = cm.getScrollInfo();
     cm.setValue(code);
     cm.scrollTo(left, top);
 }

 function update_query_string(object) {
     const url = new URL(document.location);
     Object.entries(object).forEach(([name, value]) => {
         if (value) {
             url.searchParams.set(name, value);
         } else {
             url.searchParams.delete(name);
         }
     });
     const newUrl = url.toString();
     if (newUrl !== location.href) {
         history.pushState({}, '', newUrl);
     }
 }

 function download(blob, filename) {
     const url = URL.createObjectURL(blob);
     const $a = $(`<a href="${url}" download="${filename}">download</a>`).hide();
     $a.appendTo('body');
     $a[0].click();
     $a.remove();
     URL.revokeObjectURL(url);
 }

 function base_name(filename) {
     return filename.replace(/\.[^.]+$/, '');
 }

 (async function($) {
     const has_worker = !!worker;
     if (has_worker) {
         frame.src = 'about:blank'
     }
     await worker;
     const media_query = 'screen and (max-width: 900px)';
     const small = matchMedia(media_query).matches
     const orientation = small ? 'horizontal' : 'vertical';
     $('main').split({
         orientation
     });
     let editor_splitter;
     $('.tabs').on('click', 'li', function() {
         var self = $(this);
         tab_activate(this);
         return false;
     });
     var $dev_toggle = $('#developer-mode').on('change', function() {
         toggle_dev_mode(this.checked);
     });
     var $full_mode_toggle = $('#full-output').on('change', function() {
         toggle_full_output_mode(this.checked);
     });
     $('.errors').on('click', '.close', function() {
         $(this).closest('li').remove();
     });
     $('#reset').on('click', async function() {
         parse = (code) => gaiman.parse(code);
         state.grammar = await fetch_text(`${repo}src/grammar.peg`);
         state.code = await fetch_text(`${repo}examples/hello.gs`);
         state.js = await fetch_text(`${repo}src/prefix.js`);
         state.cm = await fetch_text(`${repo}src/codemirror.js`);
         state.peg_cm = await fetch_text(pegjs);
         update_editor(state.editors.grammar, state.grammar);
         update_editor(state.editors.js, state.js);
         update_editor(state.editors.css, css.innerHTML);
         update_editor(state.editors.html, html.innerHTML);
         update_editor(state.editors.input, state.code);
         update_editor(state.editors.cm, state.cm);
         update_editor(state.editors.peg_cm, state.peg_cm);
         return false;
     });
     $(example).on('change', async function() {
         const value = this.value;
         if (value) {
             state.code = await fetch_text(`${repo}/examples/${value}`)
             update_editor(state.editors.input, state.code);
             update_query_string({gaiman: value});
         }
     });
     $(theme).on('change', async function() {
         const value = this.value;
         if (value) {
             state.css = await fetch_text(`${repo}/examples/css/${value}.css`);
             state.user_html = await fetch_text(`${repo}/examples/html/${value}.html`);
             update_editor(state.editors.css, state.css);
             setTimeout(() => {
                 update_editor(state.editors.html, state.user_html);
             }, 10);
             update_query_string({css: `${value}.css`, html: `${value}.html`});
         } else {
             update_editor(state.editors.css, css.innerHTML);
             update_editor(state.editors.html, html.innerHTML);
             update_query_string({css: '', html: ''});
         }
     });
     $('#download').on('click', function() {
         const zip = new JSZip();
         const game = zip.folder("gaiman");
         game.file("index.js", state.output_full);
         game.file("index.html", get_html('index.js', 'index.module.js'));
         zip.generateAsync({ type:"blob" }).then(function(content) {
             download(content, "gaiman.zip");
         });
     });

     const { get, set } = idbKeyval;

     const DEV_MODE = 'gaiman__dev__mode';
     const FULL_MODE = 'gaiman__full_output';
     const SELECTED_TAB = 'gaiman__selected_tab';
     const SOURCE_FILE = '_gaiman_source.gs';
     const GRAMMAR_FILE = '_gaiman_grammar.peg';
     const OUTPUT_FILE = '_gaiman_output.js';
     const OUTPUT_ES5_FILE = '_gaiman_es5_output.js';
     const CODEMIRROR_FILE = '_gaiman_cm.js';
     const PEG_CODEMIRROR_FILE = '_gaiman_peg_cm.js';
     const CSS_FILE = '_gaiman.css';
     const SCRIPT_FILE = '_gaiman.js';
     const HTML_FILE = '_gaiman.html';
     const USER_HTML = '_user.html';

     set(OUTPUT_ES5_FILE, '// this file intentionally left blank in Playground');

     query_mapping[CSS_FILE] = 'css';
     query_mapping[SOURCE_FILE] = 'gaiman';
     query_mapping[USER_HTML] = 'html';

     const REF = await hash('master');
     const repo = `https://cdn.jsdelivr.net/gh/jcubic/gaiman@${REF}/`;
     const pegjs = 'https://cdn.jsdelivr.net/npm/codemirror@5.x.x/mode/pegjs/pegjs.js';
     const state = {
         dev_mode: ls.get(DEV_MODE),
         selected_tab: ls.get(SELECTED_TAB),
         full_output_mode: ls.get(FULL_MODE),
         css: await get_template(CSS_FILE, `${repo}examples/css/`, css),
         user_html: await get_template(USER_HTML, `${repo}examples/html/`, html),
         js: await get_file(SCRIPT_FILE, `${repo}src/prefix.js`),
         editors: {},
         grammar: await get_file(GRAMMAR_FILE, `${repo}src/grammar.peg`),
         cm: await get_file(CODEMIRROR_FILE, `${repo}src/codemirror.js`),
         code: await get_file(SOURCE_FILE, `${repo}examples/`, 'hello.gs'),
         peg_cm: await get_file(PEG_CODEMIRROR_FILE, pegjs),
         ast: {}
     };
     window.state = state;

     const tempate_html = add_version(await fetch_text(`${repo}src/terminal.html`));
     const postfix = await fetch_text(`${repo}src/postfix.js`);

     let parse = (code) => gaiman.parse(code);

     state.editors.input = make_editor('.gaiman-code', state.code, {
         readOnly: !has_worker,
         mode: 'gaiman'
     });
     state.editors.js = make_editor('.js-editor', state.js, {
         readOnly: false,
         mode: 'javascript'
     });
     state.editors.explorer = {};
     ['js', 'ast'].forEach(name => {
         state.editors.explorer[name] = make_editor(`.explorer-${name}`, '', {
             readOnly: name === 'ast',
             mode: 'javascript'
         });
     });
     state.editors.explorer.js.on('change', () => {
         try {
             const code = state.editors.explorer.js.getValue();
             const ast = esprima.parseScript(code);
             update_editor(state.editors.explorer.ast, to_json(ast));
         } catch(e) {
             // invalid syntax
         }
     });
     make_parser();
     update_codemirror();
     compile();
     get_examples().then(list => {
         const selected_code = query.gaiman || 'hello.gs';
         list.forEach(name => {
             const selected = selected_code === name ? ' selected ' : '';
             $(`<option value="${name}"${selected}>${name}</option>`).appendTo(example);
         });
     });
     get_examples('examples/html', /\.html$/).then(list => {
         const css = query.css ? base_name(query.css) : '';
         const html = query.html ? base_name(query.html) : '';
         const selected_theme = css === html ? css : null;
         console.log([''].concat(list));
         [''].concat(list).map(base_name).forEach(name => {
             const selected = selected_theme === name ? ' selected ' : '';
             $(`<option value="${name}"${selected}>${name || 'default'}</option>`).appendTo(theme);
         });
     });
     state.editors.output = make_editor('.output', state.output);
     state.editors.ast = make_editor('.ast', to_json(state.ast || {}));
     state.editors.grammar = make_editor('.gaiman-grammar', state.grammar, {
         readOnly: false,
         mode: 'pegjs'
     });
     state.editors.css = make_editor('.css-editor', state.css, {
         readOnly: false,
         mode: 'css'
     });
     state.editors.html = make_editor('.html-editor', state.user_html, {
         readOnly: false,
         mode: 'htmlmixed'
     });
     state.editors.cm = make_editor('.cm-editor', state.cm, {
         readOnly: false,
         mode: 'javascript'
     });
     state.editors.peg_cm = make_editor('.peg-cm-editor', state.peg_cm, {
         readOnly: false,
         mode: 'javascript'
     });
     update_html(true);
     set_idb(true);
     load(true);
     window.addEventListener('message', async function(event) {
         const { data } = event;
         if (data && data.message) {
             const { lineno, colno, message, source } = data;
             if (lineno === null || colno === null) {
                 return error('.gaiman-output', `Error: ${message}`);
             }
             const msg = `Error: ${message}\nline: ${lineno} col: ${colno}`;
             error('.gaiman-output', msg);
             if (state.full_output_mode) {
                 const code = await fetch(source).then(res => res.text());
                 const lines = code.split('\n');
                 const line = lines[lineno - 1];
                 var start = {
                     line: lineno -1,
                     ch: line.match(/^(\s*)/)[0].length
                 }
                 const end = { line: lineno - 1, ch: line.length };
                 state.editors.output.scrollIntoView(start);
                 mark_error(
                     state.editors.output,
                     message,
                     start,
                     end
                 );
             }
         }
     });
     ['input', 'js'].forEach(name => {
         state.editors[name].on('change', debounce(update, 800));
     });
     ['css', 'html'].forEach(name => {
         state.editors[name].on('change', debounce(() => update_html(), 800));
     });

     state.editors.cm.on('change', debounce(async () => {
         state.cm = state.editors.cm.getValue();
         update_codemirror();
     }, 800));

     state.editors.peg_cm.on('change', debounce(async () => {
         state.peg_cm = state.editors.peg_cm.getValue();
         update_peg_codemirror();
     }, 800));

     state.editors.grammar.on('change', debounce(function() {
         if (state.dev_mode) {
             state.grammar = state.editors.grammar.getValue();
             clear_error('.gaiman-grammar');
             try {
                 make_parser();
                 update();
             } catch(e) {
                 parse = null;
                 error('.gaiman-grammar', e.message);
                 set(GRAMMAR_FILE, state.grammar);
             }
         }
     }, 800));

     if (state.dev_mode !== undefined) {
         toggle_dev_mode(state.dev_mode);
         $dev_toggle.prop('checked', state.dev_mode);
         if (state.full_output_mode !== undefined) {
             toggle_full_output_mode(state.full_output_mode);
             $full_mode_toggle.prop('checked', state.full_output_mode);
         }
     }

     function make_parser() {
         if (state.dev_mode) {
             const parser = peggy.generate(state.grammar);
             parse = (code) => parser.parse(code);
         }
     }

     function toggle_dev_mode(enable) {
         state.dev_mode = enable;
         ls.set(DEV_MODE, state.dev_mode);
         $('.dev').toggleClass('hidden', !state.dev_mode);
         if (enable) {
             if (!editor_splitter) {
                 editor_splitter = $('.editor-wrapper').split({
                     orientation: 'horizontal'
                 });
             }
         } else if (editor_splitter) {
             editor_splitter.destroy();
             editor_splitter = null;
             tab_activate($('.tabs li:not(.dev):eq(0)'));
         }
         $('.editor-wrapper .CodeMirror').cm_refresh();
     }

     function toggle_full_output_mode(enable) {
         state.full_output_mode = enable;
         const { full_output_mode, output_full, output } = state;
         ls.set(FULL_MODE, full_output_mode);
         update_editor(state.editors.output, full_output_mode ? output_full : output);
     }

     function update_code() {
         const prefix = add_version(state.editors.js.getValue());
         state.output_full = prefix + state.output + postfix;
     }

     function template(string, mapping) {
         Object.entries(mapping).forEach(([key, value]) => {
             string = string.replace(new RegExp(`\\{\\{${key}\\}\\}`, 'g'), value);
         });
         return string;
     }

     function add_version(code) {
         return template(code, { VER: gaiman.version });
     }

     function compile() {
         state.code = state.editors.input.getValue();
         parse_error();
         try {
             if (parse) {
                 state.ast = parse(state.code);
                 state.output = gaiman.generate(state.ast);
                 update_code();
             }
         } catch(e) {
             state.output = '';
             parse_error(state.code, e);
         }
     }

     function set_idb(init) {
         if (!init) {
             set(SOURCE_FILE, state.code);
             set(GRAMMAR_FILE, state.grammar);
             set(USER_HTML, state.user_html);
         }
         set(SCRIPT_FILE, state.js);
         set(OUTPUT_FILE, state.output_full);
     }

     async function update_html(init) {
         build_html();
         if (!init) {
             await set(CSS_FILE, state.css);
             await set(USER_HTML, state.user_html);
         }
         await set(HTML_FILE, state.html);
         frame.src = `./__idb__/${HTML_FILE}`;
     }

     async function update_codemirror() {
         if (state.dev_mode) {
             await set(CODEMIRROR_FILE, state.cm);
             await $.getScript(`./__idb__/${CODEMIRROR_FILE}`);
             state.editors.input.setOption('mode', 'text');
             setTimeout(() => state.editors.input.setOption('mode', 'gaiman'), 100);
         }
     }

     async function update_peg_codemirror() {
         if (state.dev_mode) {
             await set(PEG_CODEMIRROR_FILE, state.peg_cm);
             await $.getScript(`./__idb__/${PEG_CODEMIRROR_FILE}`);
             state.editors.grammar.setOption('mode', 'text');
             setTimeout(() => state.editors.grammar.setOption('mode', 'pegjs'), 100);
         }
     }

     function build_html() {
         state.js = state.editors.js.getValue();
         state.css = state.editors.css.getValue();
         state.user_html = state.editors.html.getValue();
         clear_error('.gaiman-output');
         clear_marks(state.editors.output);
         state.html = get_html();
     }

     function get_html(module_file = OUTPUT_FILE, es5_file = OUTPUT_ES5_FILE) {
         return template(tempate_html, {
             STYLE: css_sanitize(state.css),
             HTML: state.user_html + '\n',
             FILE: module_file,
             MODULE: module_file,
             ES5: es5_file
         });
     }

     function update() {
         build_html();
         compile();
         set_idb();
         load();
     }

     function load(init) {
         if (has_worker && state.output) {
             update_html(init);
             if (state.full_output_mode) {
                 update_editor(state.editors.output, state.output_full);
             } else {
                 update_editor(state.editors.output, state.output);
             }
             update_editor(state.editors.ast, to_json(state.ast || {}));
         }
     }
 })(jQuery);
</script>
<script defer src="https://umami.jcubic.pl/script.js" data-website-id="2ca2bdaa-ba87-41e4-9472-a55d81fb760b"></script>
<script defer async src="https://cdn.jsdelivr.net/npm/jszip"></script>
<script defer async src="https://cdn.jsdelivr.net/npm/esprima@4.x.x/dist/esprima.js"></script>
</body>
</html>
